package jp.dodododo.aop.impl;

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;

import javassist.CtBehavior;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtMethod;
import javassist.NotFoundException;
import jp.dodododo.aop.Aspect;
import jp.dodododo.aop.annotation.Enhance;
import jp.dodododo.aop.xml.XmlElement;
import jp.dodododo.janerics.GenericsMethodUtil;

@Enhance(false)
public class AspectImpl implements Aspect, Serializable, XmlElement {

	private static final long serialVersionUID = 1833775401557179140L;

	private String interceptorStatement;

	private Pattern[] patterns;

	private Pattern[] ignorePatterns;

	private int accessModifier;

	private int lineNumber;

	private boolean defaultPointcut;

	public AspectImpl(String[] pointcutPatterns, String[] ignorePatterns, String accessModifier, boolean defaultPointcut,
			int lineNumber) {
		init(pointcutPatterns, ignorePatterns, accessModifier);
		this.defaultPointcut = defaultPointcut;
		this.lineNumber = lineNumber;
	}

	private void init(String[] pointcutPatterns, String[] ignorePatterns, String accessModifier) {
		setPointcutPatterns(pointcutPatterns);
		setIgnorePatterns(ignorePatterns);
		this.accessModifier = toIntModifier(accessModifier);
	}

	/**
	 * public : Modifier.PUBLIC<br>
	 * protected : Modifier.PROTECTED<br>
	 * package-private : 0<br>
	 * private : Modifier.PRIVATE<br>
	 * 
	 * @param accessModifier
	 * @return
	 */
	private static int toIntModifier(String accessModifier) {
		int accessMod = Modifier.PUBLIC;
		if ("public".equals(accessModifier)) {
			accessMod = Modifier.PUBLIC;
		} else if ("protected".equals(accessModifier)) {
			accessMod = Modifier.PROTECTED;
		} else if ("package-private".equals(accessModifier)) {
			accessMod = Modifier.PUBLIC & Modifier.PRIVATE;
		} else if ("private".equals(accessModifier)) {
			accessMod = Modifier.PRIVATE;
		} else {
			throw new IllegalArgumentException("modifier : " + accessModifier);
		}
		return accessMod;
	}

	public String getInterceptorStatement() {
		return interceptorStatement;
	}

	public int getAccessModifier() {
		return accessModifier;
	}

	public int getLineNumber() {
		return lineNumber;
	}

	private void setIgnorePatterns(String[] ignorePatterns) {
		this.ignorePatterns = new Pattern[ignorePatterns.length];
		for (int i = 0; i < ignorePatterns.length; ++i) {
			this.ignorePatterns[i] = Pattern.compile(ignorePatterns[i].trim());
		}
	}

	private void setPointcutPatterns(String[] pointcutPatterns) {
		patterns = new Pattern[pointcutPatterns.length];
		for (int i = 0; i < pointcutPatterns.length; ++i) {
			patterns[i] = Pattern.compile(pointcutPatterns[i].trim());
		}
	}

	public boolean isApplied(Method method) {
		String methodName = method.getName();
		if (defaultPointcut == true) {
			if (isApplied(methodName) == false) {
				return false;
			}
			return isImplMethod(method);
		}
		return isApplied(methodName);
	}

	private boolean isImplMethod(Method method) {
		return GenericsMethodUtil.isImplMethod(method);
	}

	public boolean isApplied(Constructor<?> constructor) {
		if (defaultPointcut == true) {
			return false;
		}
		return isApplied((CtConstructor) null);
	}

	public boolean isApplied(CtBehavior methodOrConstructor) {
		if (methodOrConstructor instanceof CtMethod) {
			return isApplied((CtMethod) methodOrConstructor);
		} else if (methodOrConstructor instanceof CtConstructor) {
			return isApplied((CtConstructor) methodOrConstructor);
		} else {
			throw new UnsupportedOperationException("UnsupportedType.[" + methodOrConstructor.getClass() + "]");
		}
	}

	public boolean isApplied(CtConstructor constructor) {
		return isApplied("this");
	}

	public boolean isApplied(CtMethod method) {
		String methodName = method.getName();
		if (defaultPointcut == true) {
			if (isApplied(methodName) == false) {
				return false;
			}
			CtClass declaringClass = method.getDeclaringClass();
			try {
				List<CtClass> interfaces = getAllInterfaces(declaringClass);
				int paramSize = method.getParameterTypes().length;
				if (containsSameNameAndParamSizeMethod(methodName, paramSize, interfaces)) {
					return true;
				}
			} catch (NotFoundException ignore) {
				return true;
			}
			return false;
		}
		return isApplied(methodName);
	}

	private boolean containsSameNameAndParamSizeMethod(String methodName, int paramSize, List<CtClass> interfaces)
			throws NotFoundException {
		for (Iterator<CtClass> iter = interfaces.iterator(); iter.hasNext();) {
			CtClass interfaceClass = iter.next();
			CtMethod[] declaredMethods = interfaceClass.getDeclaredMethods();
			for (int i = 0; i < declaredMethods.length; i++) {
				CtMethod method = declaredMethods[i];
				if (methodName.equals(method.getName()) == true && method.getParameterTypes().length == paramSize) {
					return true;
				}
			}
		}
		return false;
	}

	private List<CtClass> getAllInterfaces(CtClass clazz) throws NotFoundException {
		List<CtClass> interfaceList = new ArrayList<CtClass>();
		addInterfaces(clazz, interfaceList);
		return interfaceList;
	}

	private void addInterfaces(CtClass clazz, List<CtClass> interfaceList) throws NotFoundException {
		if (clazz == null) {
			return;
		}
		if (clazz.isInterface() == true) {
			interfaceList.add(clazz);
		}
		CtClass[] interfaces = clazz.getInterfaces();
		for (int i = 0; i < interfaces.length; i++) {
			interfaceList.add(interfaces[i]);
		}
		CtClass superclass = clazz.getSuperclass();
		if (superclass != null) {
			addInterfaces(superclass, interfaceList);
		}
	}

	private boolean isApplied(String methodName) {
		for (int i = 0; i < ignorePatterns.length; ++i) {
			if (ignorePatterns[i].matcher(methodName).matches()) {
				return false;
			}
		}
		for (int i = 0; i < patterns.length; ++i) {
			if (patterns[i].matcher(methodName).matches()) {
				return true;
			}
		}
		return false;
	}

	public Pattern[] getPatterns() {
		return patterns;
	}

	public Pattern[] getIgnorePatterns() {
		return ignorePatterns;
	}

	public void setInterceptorStatement(String interceptorStatement) {
		if (interceptorStatement == null || interceptorStatement.trim().length() < 1) {
			throw new NullPointerException("interceptor should not be empty. lineNo=[" + getLineNumber() + "]");
		}
		this.interceptorStatement = interceptorStatement;
	}

	public boolean isDefaultPointcut() {
		return defaultPointcut;
	}

	@Override
	public boolean equals(Object o) {
		try {
			AspectImpl target = (AspectImpl) o;
			if (this.lineNumber != target.lineNumber) {
				return false;
			}
			if (this.accessModifier != target.accessModifier) {
				return false;
			}
			if (this.defaultPointcut != target.defaultPointcut) {
				return false;
			}
			if (this.interceptorStatement.equals(target.interceptorStatement) == false) {
				return false;
			}
			if (toListOfString(this.patterns).equals(toListOfString(target.patterns)) == false) {
				return false;
			}
			if (toListOfString(this.ignorePatterns).equals(toListOfString(target.ignorePatterns)) == false) {
				return false;
			}
			return true;
		} catch (ClassCastException e) {
			return false;
		}
	}

	private List<String> toListOfString(Pattern[] patterns) {
		List<String> ret = new ArrayList<String>();
		for (int i = 0; i < patterns.length; i++) {
			ret.add(patterns[i].pattern());
		}
		return ret;
	}

	@Override
	public int hashCode() {
		return lineNumber;
	}
}
